// Copyright (c) .NET Foundation and contributors. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.Collections.Generic;
using System.Linq;
using Mono.Cecil;
using Mono.Cecil.Cil;

namespace TLens.Analyzers
{
    sealed class RedundantFieldInitializationAnalyzer : Analyzer
    {
        readonly Dictionary<MethodDefinition, List<FieldDefinition>> ctors = new Dictionary<MethodDefinition, List<FieldDefinition>>();

        protected override void ProcessMethod(MethodDefinition method)
        {
            switch (method.Name)
            {
                case ".ctor":
                    RedundantInitializationToDefaultValues(method);
                    break;

                case ".cctor":
                    RedundantInitializationToDefaultValues(method);
                    break;
            }
        }

        void RedundantInitializationToDefaultValues(MethodDefinition ctor)
        {
            if (ctor.DeclaringType.IsValueType)
                return;

            var instrs = ctor.Body.Instructions;

            foreach (var instr in instrs)
            {
                switch (instr.OpCode.Code)
                {
                    case Code.Stsfld:
                    case Code.Stfld:
                        FieldReference field = (FieldReference)instr.Operand;

                        switch (field.FieldType.MetadataType)
                        {
                            case MetadataType.Boolean:
                            case MetadataType.Byte:
                            case MetadataType.Char:
                            case MetadataType.Double:
                            case MetadataType.Int16:
                            case MetadataType.Int32:
                            case MetadataType.Int64:
                            case MetadataType.SByte:
                            case MetadataType.Single:
                            case MetadataType.UInt16:
                            case MetadataType.UInt32:
                            case MetadataType.UInt64:
                                if (!IsDefaultNumeric(instr.Previous))
                                    continue;

                                break;

                            case MetadataType.String:
                            case MetadataType.Class:
                            case MetadataType.Object:
                                if (instr.Previous.OpCode.Code != Code.Ldnull)
                                    continue;

                                break;

                            case MetadataType.Pointer:
                                if (instr.Previous.OpCode.Code != Code.Conv_U || instr.Previous.Previous.OpCode.Code != Code.Ldc_I4_0)
                                    continue;

                                break;

                            case MetadataType.UIntPtr:
                            case MetadataType.IntPtr:
                                if (!IsLoadIntPtrOrUIntPtrZero(instr.Previous))
                                    continue;

                                break;

                            default:
                                continue;
                        }

                        if (!ctors.TryGetValue(ctor, out var existing))
                        {
                            existing = new List<FieldDefinition>();
                            ctors.Add(ctor, existing);
                        }

                        existing.Add(field.Resolve());
                        break;
                }
            }
        }

        static bool IsDefaultNumeric(Instruction instruction)
        {
            return instruction.OpCode.Code == Code.Ldc_I4_0;
        }

        static bool IsLoadIntPtrOrUIntPtrZero(Instruction instruction)
        {
            if (instruction.OpCode.Code != Code.Ldsfld)
                return false;

            if (instruction.Operand is not FieldReference fr || fr == null)
                return false;

            if (fr.DeclaringType.FullName != "System.IntPtr" && fr.DeclaringType.FullName != "System.UIntPtr")
                return false;

            return fr.Name == "Zero";
        }

        public override void PrintResults(int maxCount)
        {
            var entries = ctors.OrderByDescending(l => l.Value.Count).Take(maxCount);
            if (!entries.Any())
                return;

            PrintHeader("Possibly redundant fields initializations");

            foreach (var entry in entries)
            {
                Console.WriteLine($"Constructor '{entry.Key.ToDisplay()} initializes with default value fields");
                foreach (var field in entry.Value)
                    Console.WriteLine($"\tField {field.FullName}");

                Console.WriteLine();
            }
        }
    }
}
